package tv.geir.slackspace.code;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;



/**
 * Use this class to retrive a class' bytecode.
 * 
 * @author Geir Gullestad Pettersen
 *
 */
public class CachingBytecodeFinder {
	/**
	 * Logger for this class
	 */
	private static Log log = LogFactory.getLog(CachingBytecodeFinder.class);
 
	private ClassDefinitionRepository repository;
	
	private BytecodeAgent bytecodeAgent;
	
	private BytecodeClasspathScanner classpathScanner;
	
	private static CachingBytecodeFinder instance = null;
	
	private boolean useCache;
	
	private CachingBytecodeFinder() {
		repository = new ClassDefinitionRepository();
		bytecodeAgent = BytecodeAgent.getInstance();
		classpathScanner = new BytecodeClasspathScanner();
		useCache = true;
	}
	
	public static CachingBytecodeFinder getInstance() {
		if( instance == null ) {
			instance = new CachingBytecodeFinder();
		}
		return instance;
	}
	
	public byte[] findBytecode( String classname ) {
		return findClassDefinition(classname).getDefinition();
	}


	/**
	 * Returns a <code>ClassDefinition</code> object for <code>classname</code> 
	 * if the bytecode can be found. 
	 * 
	 * @param classname name of the class
	 * @return the class definition if found
	 * @throws BytecodeRetrievalException if bytecode for the specified class was not found
	 */
	public synchronized ClassDefinition findClassDefinition( String classname) throws BytecodeRetrievalException{
		validateClassname( classname );
		
		ClassDefinition cd = null;
		
		/* set first retrieval method */
		int method = useCache ? 0 : 1;
		
		/* try different ways of finding the bytecode until it is found.
		 * if we run out of methods throw an exception 
		 */
		while( cd == null ) {
			// switch to next method
			method++;
			
			switch( method ) {
			case 1:
				// fist try the repository (cache)
				cd = repository.get( classname );
				break;
			case 2:
				// if no hit, try the bytecodeagent
				cd = getClassDefinitionFromAgent(classname);
				break;
			case 3:
				// last, if not yet found do a classpath scan. 
				cd = classpathScanner.findClassDefinition(classname);
				break;
			default:
				// okay, no definition was found. throw exception
				throw createBNFException(classname);
			}			
		}
		 
		/* if class definition was not retrieved from repository, store the 
		 * class definition for future use */
		if( method > 1 )  {
			repository.put( cd );
		}
		return cd;
	}
	
	/**
	 * Creates a <code>BytecodeNotFoundException</code>
	 * 
	 * @param classname name of the class to create the exception for
	 * @return
	 */
	private BytecodeRetrievalException createBNFException( String classname ) {
		StringBuffer msg = new StringBuffer( "Bytecode for \'");
		msg.append( classname ).append( "\' was not found." );

		if( !BytecodeAgent.isLoaded() ) {
			msg.append( " NOTE: BytecodeAgent is NOT loaded. The BytecodeAgent can " )
				.append( "finds classes that are loaded into the JVM even if they're " )
				.append( "not to be found in CLASSPATH. Please consult the " )
				.append( "SlackSpaces Documentation if you want to use the BytecodeAgent.");
		}
		
		return new BytecodeRetrievalException(msg.toString());
	}
	
	/**
	 * Checks if <code>classname</code> is a valid classname, if not
	 * a <code>BytecodeNotFoundException</code> is thrown.
	 * @param classname the classname to validate
	 * @throws <code>BytecodeNotFoundException</code> if <code>classname</code> is not
	 * 		a valid classname
	 *  
	 */
	private void validateClassname( String classname ) {
		// TODO implement validateClassname()
		if( classname.indexOf('/') > 0 ) {
			
		}
	}
	
	/**
	 * Returns the classdefinition
	 * @param classname
	 * @return
	 */
	private ClassDefinition getClassDefinitionFromAgent( String classname ) {
		if( BytecodeAgent.isLoaded() ) {
			byte[] bytecode = bytecodeAgent.getByteCode(classname);
			if( bytecode != null ) {
				if( log.isDebugEnabled() ) {
					log.debug( "BytecodeAgent had class definition for " + classname );
				}
				return new ClassDefinition( classname, bytecode );
			}
		}
		return null;
	}

	protected boolean isUseCache() {
		return useCache;
	}

	protected void setUseCache(boolean useCache) {
		this.useCache = useCache;
	}
}
